#!/usr/local/easyops/python/bin/python
# -*- coding: utf-8 -*-

"""
网络设备基本信息采集。
"""

from imp import reload
import os
import sys
import traceback
import re

# 自动采集脚本无法引用lib，手动拷贝引用
sys.path.insert(0, os.path.join("/usr/local/easyops", "agent/plugins"))

from ianbai_lib.auto_collect import AutoDiscovery
from ianbai_lib.cmdb import CMDBResource
from ianbai_lib.log import get_logger
from ianbai_lib.auto_collect import snmp_walker


reload(sys)
sys.setdefaultencoding("utf-8")


logger = get_logger(__name__, "INFO")


def get_manufacturer(code):
    """根据厂商代码获取厂商名称。
    
    参数：
    - code：厂商代码；
    """
    codeMap = {
        "9": "Cisco",
        "35047": "Sangfor",
        "19849": "NSFOCUS",
        "7564": "Array",
        "28557": "Hillstone",
        "25461": "PaloAlto",
        "48797": "INOS",
        "30065": "Arista",
        "2011": "Huawei",
    }
    return codeMap.get(code, None)


def get_public_info(callSnmpwalk, collectInterface=False):
    """获取基础、接口、硬件信息。
    
    参数：
    - callSnmpwalk：函数；
    - collectInterface：是否将接口信息作为属性，默认值为False；
    """
    # 参考:http://oidref.com/1.3.6.1.2.1
    # 系统
    sysVenderOid = ".1.3.6.1.2.1.1.2.0"
    sysDesrcOid = ".1.3.6.1.2.1.1.1.0"
    sysNameOid = ".1.3.6.1.2.1.1.5.0"
    sysUptimeOid = ".1.3.6.1.2.1.1.3.0"
    # 接口
    # 接口的各类信息可能出现空值和无的情况,需要在索引号维度聚合
    ifNumOid = ".1.3.6.1.2.1.2.1"
    ifIndexOid = ".1.3.6.1.2.1.2.2.1.1"
    ifDescrOid = ".1.3.6.1.2.1.2.2.1.2"
    ifTypeOid = ".1.3.6.1.2.1.2.2.1.3"
    ifMtuOid = ".1.3.6.1.2.1.2.2.1.4"
    ifSpeedOid = ".1.3.6.1.2.1.2.2.1.5"
    # san交换机中是wwpn,网络交换机中是mac
    ifPhysAddressOid = ".1.3.6.1.2.1.2.2.1.6"
    ifAdminStatusOid = ".1.3.6.1.2.1.2.2.1.7"
    ifOperStatusOid = ".1.3.6.1.2.1.2.2.1.8"
    ifNameOid = "1.3.6.1.2.1.31.1.1.1.1"
    ifAliasOid = ".1.3.6.1.2.1.31.1.1.1.18"

    # 参考:http://oidref.com/1.3.6.1.2.1.47.1.1.1.1
    # 实体
    # 实体信息与接口类似,输出硬件的索引和序列号,主设备取第一个不为空的值
    # 思科san交换机没有索引号,从描述信息中获取索引号
    entPhysicalDescrOid = "1.3.6.1.2.1.47.1.1.1.1.2"
    entPhysicalContainedInOid = "1.3.6.1.2.1.47.1.1.1.1.4"
    entPhysicalParentRelPosOid = "1.3.6.1.2.1.47.1.1.1.1.6"
    entPhysicalNameOid = "1.3.6.1.2.1.47.1.1.1.1.7"
    entPhysicalSoftwareRevOid = "1.3.6.1.2.1.47.1.1.1.1.10"
    entPhysicalSerialNumOid = "1.3.6.1.2.1.47.1.1.1.1.11"
    entPhysicalMfgNameOid = "1.3.6.1.2.1.47.1.1.1.1.12"
    entPhysicalModelNameOid = "1.3.6.1.2.1.47.1.1.1.1.13"
    entPhysicalAliasOid = "1.3.6.1.2.1.47.1.1.1.1.14"

    data = {}

    def __re_search(rec, str):
        res = re.search(rec, str)
        if res:
            return res.group(1)
        else:
            return ""

    # 系统信息
    # SNMPv2-MIB::sysObjectID.0 = OID: SNMPv2-SMI::enterprises.9.12.3.1.3.1491
    cal = callSnmpwalk(sysVenderOid)
    rec = re.compile(r".*enterprises\.([0-9]+)", re.I)
    res = __re_search(rec, cal)
    data["manufacturer"] = get_manufacturer(res) or res
    
    # SNMPv2-MIB::sysDescr.0 = STRING: Cisco NX-OS(tm) m9100, Software (m9100-s5ek9-mz), Version 8.4(2c), RELEASE SOFTWARE Copyright (c) 2002-2021 by Cisco Systems, Inc. Compiled 3/30/2021 23:00:00
    cal = callSnmpwalk(sysDesrcOid)
    # 考虑描述信息多行输出的情况
    rec = re.compile(r".*STRING: (.+)", re.S)
    res = __re_search(rec, cal)
    
    # 替换描述中的空字符,否则写入数据被截断
    rec = re.compile(r"\s+")
    res = re.sub(rec, " ", res)
    data["sysDescr"] = res.strip(" ")
    
    # SNMPv2-MIB::sysName.0 = STRING: cfzq-mgsg-9148-1
    cal = callSnmpwalk(sysNameOid)
    rec = re.compile(r".*STRING: (.+)")
    res = __re_search(rec, cal)
    
    # 替换空字符串
    rec = re.compile(r"\s+")
    res = re.sub(rec, "", res)
    data["sysName"] = res

    # DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (1684447087) 194 days, 23:01:10.87
    # 如果不满1天,没有天数字段,默认值为1
    cal = callSnmpwalk(sysUptimeOid)
    rec = re.compile(r" (\d+) days")
    res = __re_search(rec, cal)
    data["sysUptime"] = int(res or "1")

    # IF-MIB::ifNumber.0 = INTEGER: 137
    cal = callSnmpwalk(ifNumOid)
    rec = re.compile(r" (\d+)")
    res = __re_search(rec, cal)
    # 接口数量可能为空字符串
    data["ifNum"] = int(res) if res else None
    
    def __get_if(str):
        """将接口信息转换为以索引号为键的字典。"""
        # 各项接口信息的输出并不与索引号完全对齐,必须根据索引号进行匹配组合
        rec = re.compile(r".*\.(\d*) .*: (.*)", re.M)
        res = re.findall(rec, str)
        return dict(res)
    
    def __del_brackets(str):
        """删除行尾圆括号。"""
        rec = re.compile(r"\(.*\)$", re.M)
        res = re.sub(rec, "", str)
        return res
    
    def __format_phyadd(str):
        if str:
            tmp = str.split(":")
            res = []
            for i in tmp:
                res.append(i.rjust(2, "0"))
            res = ":".join(res)
            return res.upper()
        else:
            return ""

    # 接口
    if collectInterface:
        ifTmp = {}
        ifInfo = []

        # IF-MIB::ifDescr.16777216 = STRING: fc1/1
        cal = callSnmpwalk(ifDescrOid)
        ifTmp["ifDescr"] = __get_if(cal)

        # IF-MIB::ifType.16777216 = INTEGER: fibreChannel(56)
        cal = callSnmpwalk(ifTypeOid)
        cal = __del_brackets(cal)
        ifTmp["ifType"] = __get_if(cal)

        # IF-MIB::ifMtu.16777216 = INTEGER: 2112
        cal = callSnmpwalk(ifMtuOid)
        ifTmp["ifMtu"] = __get_if(cal)

        # IF-MIB::ifSpeed.16777216 = Gauge32: 4294967295
        cal = callSnmpwalk(ifSpeedOid)
        ifTmp["ifSpeed"] = __get_if(cal)

        # IF-MIB::ifPhysAddress.16777216 = STRING: 20:1:0:de:fb:94:e:20
        cal = callSnmpwalk(ifPhysAddressOid)
        ifTmp["ifPhysAddress"] = __get_if(cal)

        # IF-MIB::ifAdminStatus.16777216 = INTEGER: up(1)
        cal = callSnmpwalk(ifAdminStatusOid)
        cal = __del_brackets(cal)
        ifTmp["ifAdminStatus"] = __get_if(cal)

        # IF-MIB::ifOperStatus.16777216 = INTEGER: up(1)
        cal = callSnmpwalk(ifOperStatusOid)
        cal = __del_brackets(cal)
        ifTmp["ifOperStatus"] = __get_if(cal)

        # IF-MIB::ifName.1 = STRING: Gi0/0
        cal = callSnmpwalk(ifNameOid)
        ifTmp["ifName"] = __get_if(cal)

        # IF-MIB::ifAlias.3 = STRING: TO-CHENHAIRUO-[lianghua]
        cal = callSnmpwalk(ifAliasOid)
        ifTmp["ifAlias"] = __get_if(cal)

        # 合并接口信息
        # IF-MIB::ifIndex.16777216 = INTEGER: 16777216
        cal = callSnmpwalk(ifIndexOid)
        rec = re.compile(r".*: (\d*)", re.M)
        res = re.findall(rec, cal)
        ifIndex = res

        for i in ifIndex:
            ifInfo.append({
                # 保留接口索引,作为合并品牌私有接口信息的依据
                "index": i,
                "descr": ifTmp["ifDescr"].get(i, ""),
                "type": ifTmp["ifType"].get(i, ""),
                "mtu": "" if not ifTmp["ifMtu"].get(i) else int(ifTmp["ifMtu"].get(i)),
                "speed": "" if not ifTmp["ifSpeed"].get(i) else int(ifTmp["ifSpeed"].get(i)),
                "mac": __format_phyadd(ifTmp["ifPhysAddress"].get(i, "")),
                "adminStatus": ifTmp["ifAdminStatus"].get(i, "").upper(),
                "opStatus": ifTmp["ifOperStatus"].get(i, "").upper(),
                "name": ifTmp["ifName"].get(i, ""),
                "alias": ifTmp["ifAlias"].get(i, ""),
            })
        
        data["ifInfo"] = ifInfo

    # 硬件
    entTmp = {}
    entInfo = []

    def __get_ent(str):
        """将硬件信息转换为以索引号为键的字典。"""
        rec = re.compile(r".*\.(\d*) .*: (.*)", re.M)
        res = re.findall(rec, str)

        # 序列的第一个值
        try:
            firstRes = res[0][1]
        except:
            firstRes = None
        
        return firstRes, dict(res)
    
    def __del_quota(str):
        """删除双引号."""
        rec = re.compile(r"\"", re.M)
        res = re.sub(rec, "", str)
        return res
    
    # SNMPv2-SMI::mib-2.47.1.1.1.1.2.10 = STRING: "Fabric Stack Root"
    cal = callSnmpwalk(entPhysicalDescrOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalDescr"] = __get_ent(cal)[1]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.4.10 = INTEGER: 0
    cal = callSnmpwalk(entPhysicalContainedInOid)
    entTmp["entPhysicalContainedIn"] = __get_ent(cal)[1]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.6.10 = INTEGER: -1
    cal = callSnmpwalk(entPhysicalParentRelPosOid)
    entTmp["entPhysicalParentRelPos"] = __get_ent(cal)[1]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.7.10 = STRING: "Fabric Stack Root"
    cal = callSnmpwalk(entPhysicalNameOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalName"] = __get_ent(cal)[1]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.10.10 = ""
    cal = callSnmpwalk(entPhysicalSoftwareRevOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalSoftwareRev"] = __get_ent(cal)[1]
    data["version"] = __get_ent(cal)[0]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.11.10 = STRING: "JPG212100NN"
    cal = callSnmpwalk(entPhysicalSerialNumOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalSerialNum"] = __get_ent(cal)[1]
    data["sn"] = __get_ent(cal)[0]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.12.10 = STRING: "Cisco Systems, Inc."
    cal = callSnmpwalk(entPhysicalMfgNameOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalMfgName"] = __get_ent(cal)[1]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.13.10 = STRING: "DS-C9148S-K9"
    cal = callSnmpwalk(entPhysicalModelNameOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalModelName"] = __get_ent(cal)[1]
    data["model"] = __get_ent(cal)[0]

    # SNMPv2-SMI::mib-2.47.1.1.1.1.14.10 = ""
    cal = callSnmpwalk(entPhysicalAliasOid)
    cal = __del_quota(cal)
    entTmp["entPhysicalAlias"] = __get_ent(cal)[1]

    entIndex = list(entTmp["entPhysicalDescr"].keys())

    # 合并硬件信息
    for i in entIndex:
        entInfo.append({
            "descr": entTmp["entPhysicalDescr"].get(i, "").strip(),
            "name": entTmp["entPhysicalName"].get(i, "").strip(),
            "version": entTmp["entPhysicalSoftwareRev"].get(i, "").strip(),
            "sn": entTmp["entPhysicalSerialNum"].get(i, "").strip(),
            "mfgName": entTmp["entPhysicalMfgName"].get(i, "").strip(),
            "model": entTmp["entPhysicalModelName"].get(i, "").strip(),
            "alias": entTmp["entPhysicalAlias"].get(i, "").strip(),
            # 硬件安装位置
            # entPhysicalContainedIn指向的硬件可能不存在,此时entPhysicalParentRelPos值为-1
            "insPos": "%s | %s" % (entTmp["entPhysicalName"].get(entTmp["entPhysicalContainedIn"].get(i), "").strip(),
                                   entTmp["entPhysicalParentRelPos"].get(i))
        })
    
    data["entInfo"] = entInfo

    return data


AutoDiscovery = AutoDiscovery()


def main(modelId, collectInterface=False):
    netdObj = CMDBResource(modelId, EASYOPS_ORG)

    query = {
        "isCollect": {"$eq": "是"},
        "collectHost": {"$eq": EASYOPS_LOCAL_IP},
        "snmpVer": {"$exists": True},
        "$or": [
            {"inBandIp": {"$exists": True}},
            {"outBandIp": {"$exists": True}},
        ]
    }
    fields = ["name", "inBandIp", "outBandIp", "snmpVer", "snmpInfo2c", "snmpInfo3"]

    logger.info(u"开始获取采集对象")

    netds = netdObj.get_all(netdObj.search_v3, query=query, fields=fields)

    logger.info(u"获取成功，对象数量：%s" % len(netds))

    netdReportList = []
    for netd in netds:
        # 优先使用带外IP
        host = netd.get("outBandIp") or netd.get("inBandIp")

        # 验证必填信息，避免不必要的采集
        # snmp 1或2c
        if netd.get("snmpVer") in ["1", "2c"] and not netd["snmpInfo2c"]["community"]:
            logger.warning(u"团体字为空")
            continue
        
        # snmp 3
        # TODO：补充校验逻辑
        if netd.get("snmpVer") == "3":
            pass

        logger.info(u"正在采集：%s" % host)

        callSnmpwalk = snmp_walker(host, netd.get("snmpVer"),
                                   community=netd.get("snmpInfo2c", {}).get("community"),
                                   authProtocol=netd.get("snmpInfo3", {}).get("authProtocol"),
                                   level=netd.get("snmpInfo3", {}).get("level"),
                                   context=netd.get("snmpInfo3", {}).get("context"),
                                   username=netd.get("snmpInfo3", {}).get("username"),
                                   authPasswd=netd.get("snmpInfo3", {}).get("authPasswd"),
                                   privProtocol=netd.get("snmpInfo3", {}).get("privProtocol"),
                                   privPasswd=netd.get("snmpInfo3", {}).get("privPasswd"))
        
        try:
            publicInfo = get_public_info(callSnmpwalk, collectInterface)
            publicInfo["name"] = netd["name"]
            
            # 过滤属性为空的键,避免采集空值覆盖已有值
            noneValeFilter = True
            data = {}
            for k, v in publicInfo.items():
                if v or not noneValeFilter:
                    data[k] = v
            
            netdReportList.append(data)
        except Exception as e:
            logger.error(u"意料之外的错误：%s" % e)
            logger.debug(u"错误详情：%s" % traceback.format_exc())
            continue

    AutoDiscovery.data_format(jsonData=netdReportList, objectId=modelId, pks=["name"], upsert=False)
    AutoDiscovery.report_data()


if __name__ == "__main__":
    """
    脚本输入参数：
    - inputModelId：模型ID；
    - inputCollectInterface：是否采集接口信息作为属性；
    """
    inputCollectInterface = True if inputCollectInterface == "是" else False
    
    main(inputModelId, inputCollectInterface)
